# For C++ builds, the GenRule contains the following fields.
#   .cc_so_lib: .so files that should be linked in.
#   .cc_a_lib: .a files that should be linked in.
#   .cc_o_lib: .o files that should be linked in.
#   .cc_hdrs: headers available to dependent rules.
#   .cc_cc: cc files that are provided or required by the rule.
#   .proto_proto: proto files that are provided or required by the rule.
#   .data: data to be made available at runtime.
#   .py_lib: python libraries.
# TODO(pschuh): add checks for out of directory files.

# TODO(aschuh): Need a way to verify that source files exist.
import os
import re

def __is_c_or_cc(src):
  return src[-3:] == '.cc' or src[-2:] == '.c' or src[-4:] == '.cpp'

def __to_o_file(src):
    return src[:src.rfind('.')] + '.o'

def __cc_compiler_option():
  return option('cc_compiler', None)

def __cc_compiler_environment_option():
  return option('cc_compiler_environment', None)

def __cc_ar():
  return option('cc_ar', ['ar', 'qsc'])

def __cflags_option():
  return option('cflags', ['-Werror', '-Wall', '-Wextra'])

def __ccflags_option():
  return option('ccflags', ['-std=c++11', '-Werror', '-Wall', '-Wextra'])

def __cc_o_file_raw(name, o_file, deps, hdrs=None, hdrs_fileset=None,
                    private_hdrs=None, private_hdrs_fileset=None,
                    cc_flags=None, src_fileset=None, src_filenames=None,
                    cc_compiler=None, cc_compiler_environment=None, cflags=None,
                    ccflags=None):
  if private_hdrs is None:
    private_hdrs = []
  if cc_flags is None:
    cc_flags = []
  rule = GenRule(name)
  rule.out.cc_o_lib.add(o_file)

  if src_filenames:
    rule.inp.cc_cc.add_all(src_filenames)
  if src_fileset:
    rule.inp.cc_cc.depends_on(src_fileset)
  if private_hdrs:
    rule.inp.cc_private_hdrs.add_all(private_hdrs)
  if private_hdrs_fileset:
    rule.inp.cc_private_hdrs.depends_on(private_hdrs_fileset)
  rule.out.cc_hdrs.depends_on(rule.inp.cc_hdrs)
  if hdrs:
    rule.inp.cc_hdrs.add_all(hdrs)
  if hdrs_fileset:
    rule.inp.cc_hdrs.depends_on(hdrs_fileset)

  for dep in deps:
    dep_rule = getRule(dep)
    rule.out.cc_a_lib.depends_on(dep_rule.out.cc_a_lib)
    rule.meta.cc_system_so_flags.depends_on(dep_rule.meta.cc_system_so_flags)
    rule.meta.dep_cc_flags.depends_on(dep_rule.meta.dep_cc_flags)
    rule.inp.cc_hdrs.depends_on(dep_rule.out.cc_hdrs)
    rule.out.cc_hdrs.depends_on(dep_rule.out.cc_hdrs)

  cmd = cc_compiler[:]
  env = cc_compiler_environment
  if cmd is None:
    raise RuleError('cc_compiler must be specified in the config file.')

  input_file = str(rule.inp.cc_cc)
  if input_file[-2:] == '.c':
    cmd += cflags
  else:
    cmd += ccflags

  cmd += ['-I.', '-I./' + output_basename() + '/']
  cmd += rule.meta.dep_cc_flags.file_names()
  cmd += cc_flags
  cmd += ['-fPIC', '-o', o_file, '-c', str(rule.inp.cc_cc)]
  rule.set_cmd(cmd)
  rule.set_env(env)
  return rule


def __cc_o_file(name, o_file, deps, hdrs=None, hdrs_fileset=None,
                private_hdrs=None, private_hdrs_fileset=None,
                cc_flags=None, src_fileset=None, src_filenames=None):
  return __cc_o_file_raw(name, o_file, deps, hdrs=hdrs, hdrs_fileset=hdrs_fileset,
                private_hdrs=private_hdrs, private_hdrs_fileset=private_hdrs_fileset,
                cc_flags=cc_flags, src_fileset=src_fileset, src_filenames=src_filenames,
                cc_compiler=__cc_compiler_option(),
                cc_compiler_environment=__cc_compiler_environment_option(),
                cflags=__cflags_option(),
                ccflags=__ccflags_option())

def __proto_cmd_builder(proto_cc_gen_rule=None, expanded_output=None,
                        expanded_build_src=None, protoc=None, protoc_rule=None):
  protoc_cmd = protoc[:]

  if protoc_rule is not None:
    protoc_actual_rule = getRule(protoc_rule)
    proto_cc_gen_rule.inp.compiler.depends_on(protoc_actual_rule.out.executable)
    protoc_exe_list = list(protoc_actual_rule.out.executable.file_names())
    if len(protoc_exe_list) != 1:
      raise RuleError('Found protoc %d compilers, not 1' % len(protoc_exe_list))

    protoc_cmd[0] = protoc_exe_list[0]

  protoc_cmd.append('-I.')
  protoc_cmd.append(expanded_build_src)
  protoc_cmd.append('--cpp_out=./' + output_basename() + '/')

  return (protoc_cmd,
          expanded_output.replace('.proto', '.pb.h'),
          expanded_output.replace('.proto', '.pb.cc'))

def __proto_rulename_builder(expanded_output):
  return expanded_output + '_pb_cc'

@GenRuleFunc
def __proto_gen_cc(build_src, srcs, deps=[], protoc=None, protoc_rule=None,
                   proto_cmd_builder=None,
                   proto_rulename_builder=None):
  proto_cc_gen_rule = GenRule(proto_rulename_builder(expand_out_path(build_src)))

  for dep in deps:
    dep_rule = getRule(dep)
    proto_cc_gen_rule.inp.proto_proto.depends_on(dep_rule.out.proto_proto)

  protoc_cmd, cc_hdrs, cc_cc = proto_cmd_builder(
      proto_cc_gen_rule=proto_cc_gen_rule,
      expanded_output=expand_out_path(build_src),
      expanded_build_src=expand_path(build_src),
      protoc=protoc, protoc_rule=protoc_rule)

  proto_cc_gen_rule.inp.proto_proto.depends_on(srcs)
  proto_cc_gen_rule.out.proto_proto.depends_on(
      proto_cc_gen_rule.inp.proto_proto)

  proto_cc_gen_rule.out.cc_hdrs.add(cc_hdrs)
  proto_cc_gen_rule.out.cc_cc.add(cc_cc)

  proto_cc_gen_rule.set_cmd(protoc_cmd)
  return proto_cc_gen_rule

@GenRuleFunc
def proto_library(name, srcs, deps=None):
  return proto_library_genrule(
      name, srcs, deps,
      __cc_compiler_option(),
      __cc_compiler_environment_option(),
      __cc_ar(),
      __cflags_option(),
      __ccflags_option(),
      option('proto_rule', '//base:protobuf'),
      protoc=option('protoc', ['protoc', '-I/usr/include']),
      protoc_rule=option('protoc_rule', None))

@GenRuleFunc
def proto_library_genrule(name, srcs, deps=None,
                          cc_compiler=None, cc_compiler_environment=None,
                          ar=None, cflags=None, ccflags=None,
                          proto_rule_name=None,
                          protoc=None,
                          protoc_rule=None,
                          proto_cmd_builder=__proto_cmd_builder,
                          proto_rulename_builder=__proto_rulename_builder):
  if deps is None:
    deps = [proto_rule_name]
  else:
    deps += [proto_rule_name]

  lib_rule = GenRule(name)

  proto_fileset_rule = GenRule(name + '_proto_fileset')
  proto_fileset_rule.inp.proto_proto.add_all([expand_path(src) for src in srcs])
  proto_fileset_rule.out.proto_proto.depends_on(
      proto_fileset_rule.inp.proto_proto)

  cc_gen_rules = [__proto_gen_cc(src, proto_fileset_rule.out.proto_proto,
                                 deps, protoc, protoc_rule,
                                 proto_cmd_builder=proto_cmd_builder,
                                 proto_rulename_builder=proto_rulename_builder) for src in srcs]

  cc_gen_hdrs = [rule.out.cc_hdrs for rule in cc_gen_rules]

  for cc_gen_rule, src in zip(cc_gen_rules, srcs):
    cc_list = list(cc_gen_rule.out.cc_cc.file_names())
    if len(cc_list) != 1:
      raise RuleError('Internal error, wrong number of files from protobuf cc genrule.')
    o_file = cc_list[0].replace('.pb.cc', '.pb.o')
    src_rule = __cc_o_file_raw(name=o_file, src_fileset=cc_gen_rule.out.cc_cc,
                               deps=deps, o_file=o_file,
                               cc_compiler=cc_compiler,
                               cc_compiler_environment=cc_compiler_environment,
                               cflags=cflags, ccflags=ccflags)
    for hdr in cc_gen_hdrs:
      src_rule.inp.cc_hdrs.depends_on(hdr)
    lib_rule.inp.cc_o_lib.depends_on(src_rule.out.cc_o_lib)
    lib_rule.out.cc_hdrs.depends_on(src_rule.out.cc_hdrs)
    lib_rule.inp.proto_proto.depends_on(cc_gen_rule.out.proto_proto)
    lib_rule.out.proto_proto.depends_on(cc_gen_rule.out.proto_proto)

  for dep in deps:
    dep_rule = getRule(dep)
    lib_rule.out.cc_a_lib.depends_on(dep_rule.out.cc_a_lib)
    lib_rule.meta.cc_system_so_flags.depends_on(dep_rule.meta.cc_system_so_flags)
    lib_rule.out.cc_hdrs.depends_on(dep_rule.out.cc_hdrs)
    lib_rule.meta.deb_system_dep.depends_on(dep_rule.meta.deb_system_dep)
    lib_rule.meta.dep_cc_flags.depends_on(dep_rule.meta.dep_cc_flags)

  output_library = expand_out_path('lib%s.pb.a' % name)
  lib_rule.out.cc_a_lib.add(output_library)

  cmd = ar[:]
  cmd.append(output_library)
  cmd += lib_rule.inp.cc_o_lib.file_names()
  lib_rule.set_cmd(cmd)
  return lib_rule


@GenRuleFunc
def system_package(name, package):
  rule = GenRule(name)
  rule.meta.deb_system_dep.add(package)
  return rule


@GenRuleFunc
def cc_system_library(name, lib=None, package_name=None, deps=[], hdrs=[],
                      dep_cc_flags=[]):
  rule = GenRule(name)
  rule.meta.dep_cc_flags.add_all(dep_cc_flags)
  if lib is not None:
    rule.meta.cc_system_so_flags.add('-l' + lib)
  if package_name is not None:
    rule.meta.deb_system_dep.add(package_name)

  rule.out.cc_hdrs.add_all(hdrs)
  for dep in deps:
    dep_rule = getRule(dep)

    rule.out.cc_a_lib.depends_on(dep_rule.out.cc_a_lib)
    rule.meta.cc_system_so_flags.depends_on(dep_rule.meta.cc_system_so_flags)
    rule.meta.dep_cc_flags.depends_on(dep_rule.meta.dep_cc_flags)
    rule.meta.deb_system_dep.depends_on(dep_rule.meta.deb_system_dep)
    rule.out.cc_hdrs.depends_on(dep_rule.out.cc_hdrs)
  return rule

@GenRuleFunc
def __cc_library(name, srcs=[], deps=[], hdrs=[], cc_flags=[],
                 dep_cc_flags=[], extra_src_rules=[]):
  return cc_library_genrule(name, srcs, deps, hdrs, cc_flags,
                            dep_cc_flags, extra_src_rules,
                            __cc_compiler_option(),
                            __cc_compiler_environment_option(),
                            __cc_ar(),
                            __cflags_option(),
                            __ccflags_option())

@GenRuleFunc
def cc_library_genrule(name, srcs=[], deps=[], hdrs=[], cc_flags=[],
                       dep_cc_flags=[], extra_src_rules=[], cc_compiler=None,
                       cc_compiler_environment=None, ar=None, cflags=None,
                       ccflags=None):
  rule = GenRule(name)
  expanded_hdrs = [expand_path(hdr) for hdr in hdrs]

  rule.meta.dep_cc_flags.add_all(dep_cc_flags)
  for dep in deps:
    dep_rule = getRule(dep)
    rule.out.cc_a_lib.depends_on(dep_rule.out.cc_a_lib)
    rule.meta.cc_system_so_flags.depends_on(dep_rule.meta.cc_system_so_flags)
    rule.meta.dep_cc_flags.depends_on(dep_rule.meta.dep_cc_flags)
    rule.inp.cc_hdrs.depends_on(dep_rule.out.cc_hdrs)
    rule.out.cc_hdrs.depends_on(dep_rule.out.cc_hdrs)
    rule.meta.deb_system_dep.depends_on(dep_rule.meta.deb_system_dep)

  cc_srcs = []
  h_srcs = []
  for src in srcs:
    if __is_c_or_cc(src):
      cc_srcs.append(src)
    elif src[-2:] == '.h':
      h_srcs.append(expand_path(src))
    else:
      raise RuleError('Invalid file extension for cc_library: ' + src)

  hdr_rule = None
  private_hdrs_names = h_srcs
  public_hdrs_names = expanded_hdrs
  private_hdrs_fileset = None
  public_hdrs_fileset = None
  if h_srcs or hdrs:
    if not cc_srcs and not extra_src_rules:
      # Slap the headers into the .h
      rule.inp.cc_hdrs.add_all(expanded_hdrs)
      rule.out.cc_hdrs.depends_on(rule.inp.cc_hdrs)
    else:
      hdr_rule = GenRule(name + '_hdrs')
      hdr_rule.inp.cc_hdrs.add_all(expanded_hdrs)
      hdr_rule.out.cc_hdrs.depends_on(hdr_rule.inp.cc_hdrs)
      hdr_rule.inp.cc_private_hdrs.add_all(h_srcs)
      hdr_rule.out.cc_private_hdrs.depends_on(hdr_rule.inp.cc_private_hdrs)
      private_hdrs_fileset = hdr_rule.out.cc_private_hdrs
      public_hdrs_fileset = hdr_rule.out.cc_hdrs
      private_hdrs_names = None
      public_hdrs_names = None

  if cc_srcs or extra_src_rules:
    output_library = expand_out_path('lib%s.a' % name)
    rule.out.cc_a_lib.add(output_library)

    for src_rule in extra_src_rules:
      rule.inp.cc_o_lib.depends_on(src_rule.out.cc_o_lib)
      rule.inp.cc_hdrs.depends_on(src_rule.out.cc_hdrs)
      rule.out.cc_hdrs.depends_on(src_rule.out.cc_hdrs)

    for src in cc_srcs:
      raw_o_file = __to_o_file(src)
      o_file = expand_out_path(
          os.path.join(os.path.dirname(raw_o_file),
                       name + '--' + os.path.basename(raw_o_file)))
      rule_name = name + '--' + __to_o_file(src)
      src_rule = __cc_o_file_raw(name=rule_name,
                                 src_filenames=[expand_path(src)],
                                 o_file=o_file, deps=deps,
                                 hdrs_fileset=public_hdrs_fileset,
                                 hdrs=public_hdrs_names,
                                 private_hdrs_fileset=private_hdrs_fileset,
                                 private_hdrs=private_hdrs_names,
                                 cc_flags=cc_flags,
                                 cc_compiler=cc_compiler,
                                 cc_compiler_environment=cc_compiler_environment,
                                 cflags=cflags,
                                 ccflags=ccflags)
      rule.inp.cc_o_lib.depends_on(src_rule.out.cc_o_lib)
      rule.inp.cc_hdrs.depends_on(src_rule.out.cc_hdrs)
      rule.out.cc_hdrs.depends_on(src_rule.out.cc_hdrs)

    cmd = ar[:]
    cmd.append(output_library)
    cmd += rule.inp.cc_o_lib.file_names()
    rule.set_cmd(cmd)
  return rule

@GenRuleFunc
def cc_library(name, srcs=[], deps=[], hdrs=[], cc_flags=[]):
  return __cc_library(name, srcs=srcs, deps=deps, hdrs=hdrs, cc_flags=cc_flags)

@GenRuleFunc
def cc_third_party_library(name, srcs=[], deps=[], hdrs=[], cc_flags=[], dep_cc_flags=[]):
  return __cc_library(name, srcs=srcs, deps=deps, hdrs=hdrs, cc_flags=cc_flags, dep_cc_flags=dep_cc_flags)


# TODO(aschuh): Check the graph.
@GenRuleFunc
def qt4_cc_library(name, srcs=None, deps=None, hdrs=None, cc_flags=None):
  # Find all the headers, and build them with moc.
  if cc_flags is None:
    cc_flags = []
  if deps is None:
    deps = []
  if srcs is None:
    srcs = []
  if hdrs is None:
    hdrs = []

  moc_hdrs = []
  filtered_srcs = []
  cc_flags += ['-isystem/usr/share/qt4/mkspecs/linux-g++-64',
               '-isystem/usr/include/qt4/QtCore',
               '-isystem/usr/include/qt4/QtGui',
               '-isystem/usr/include/qt4',
               '-isystem/usr/include/qt4/QtWebKit',
               '-isystem/usr/include/qt4/QtOpenGL',
               '-isystem/usr/include/qt4/QtNetwork']
  for src in srcs:
    if src[-2:] == '.h':
      moc_hdrs.append(src)
    else:
      filtered_srcs.append(src)
  for hdr in hdrs:
    if hdr[-2:] == '.h':
      moc_hdrs.append(hdr)

  moc_o_rules = []

  for moc_hdr in moc_hdrs:
    cc_file = 'moc_' + moc_hdr[:-2] + '.cc'
    moc_rule = GenRule(cc_file)
    moc_rule.out.cc_cc.add(expand_out_path(cc_file))

    moc_rule.inp.cc_hdrs.add_all([expand_path(hdr) for hdr in hdrs])
    moc_rule.out.cc_hdrs.depends_on(moc_rule.inp.cc_hdrs)

    for dep in deps:
      dep_rule = getRule(dep)
      moc_rule.out.cc_a_lib.depends_on(dep_rule.out.cc_a_lib)
      moc_rule.meta.cc_system_so_flags.depends_on(dep_rule.meta.cc_system_so_flags)
      moc_rule.inp.cc_hdrs.depends_on(dep_rule.out.cc_hdrs)
      moc_rule.out.cc_hdrs.depends_on(dep_rule.out.cc_hdrs)
      moc_rule.meta.deb_system_dep.depends_on(dep_rule.meta.deb_system_dep)

    cmd = ['/usr/bin/moc-qt4', '-DQT_WEBKIT', '-DQT_NO_DEBUG', '-DQT_GUI_LIB',
           '-DQT_CORE_LIB', '-DQT_SHARED',
           '-I/usr/share/qt4/mkspecs/linux-g++-64',
           '-I/usr/include/qt4/QtCore',
           '-I/usr/include/qt4/QtGui',
           '-I/usr/include/qt4',
           '-I/usr/include/qt4/QtWebKit',
           '-I/usr/include/qt4/QtOpenGL',
           '-I/usr/include/qt4/QtNetwork',
           expand_path(moc_hdr),
           '-o', expand_out_path(cc_file)]
    moc_rule.set_cmd(cmd)

    o_file = expand_out_path(name + '--' + __to_o_file(cc_file))

    o_rule = __cc_o_file(name=o_file, src_fileset=moc_rule.out.cc_cc, deps=deps,
                         hdrs=[expand_path(moc_hdr) for moc_hdr in moc_hdrs],
                         o_file=o_file, cc_flags=cc_flags)
    moc_o_rules.append(o_rule)

  return __cc_library(name, srcs=srcs, deps=deps, hdrs=hdrs, cc_flags=cc_flags, extra_src_rules=moc_o_rules)

@GenRuleFunc
def qt4_cc_binary(name, srcs=None, deps=None, cc_flags=None):
  if cc_flags is None:
    cc_flags = []
  cc_flags += ['-isystem/usr/share/qt4/mkspecs/linux-g++-64',
               '-isystem/usr/include/qt4/QtCore',
               '-isystem/usr/include/qt4/QtGui',
               '-isystem/usr/include/qt4',
               '-isystem/usr/include/qt4/QtWebKit',
               '-isystem/usr/include/qt4/QtOpenGL',
               '-isystem/usr/include/qt4/QtNetwork']
  return cc_binary(name, srcs=srcs, deps=deps, cc_flags=cc_flags)


@GenRuleFunc
def qt4_ui_library(name, src):
  expanded_src = expand_path(src)
  expanded_hdr = expand_out_path('ui_' + src[:-3] + '.h')
  rule = GenRule(name)
  rule.inp.qt4_ui.add(expanded_src)
  rule.out.cc_hdrs.add(expanded_hdr)
  rule.set_cmd(['/usr/bin/uic-qt4', expanded_src, '-o', expanded_hdr])
  return rule

def __resolve(lst):
  return [getRule(item) for item in lst]

__link_mapping = {
    'cc_o_lib' : ['inp', 'out'],
    'cc_a_lib' : ['inp', 'out'],
    'cc_system_so_flags' : ['meta', 'meta']
  }

def __list_graph_link(rule, lst, up_flags):
  for item in lst:
    for up_flag in up_flags:
      lval, rval = __link_mapping[up_flag]
      tree = getattr(getattr(item, rval), up_flag)
      getattr(getattr(rule, lval), up_flag).update(tree)

@GenRuleFunc
def cc_binary(name, srcs=None, deps=None, cc_flags=None):
  return cc_binary_genrule(
      name, srcs=srcs, deps=deps, cc_flags=cc_flags,
      cc_linker=option('cc_linker', None),
      cc_linker_extra_lib_flags=option('cc_linker_extra_lib_flags',
                                       ['-lm', '-lstdc++']),
      objcopy=option('cc_objcopy', ['objcopy']),
      strip=option('cc_strip', ['strip']),
      cc_compiler=__cc_compiler_option(),
      cc_compiler_environment=__cc_compiler_environment_option(),
      cflags=__cflags_option(),
      ccflags=__ccflags_option())


@GenRuleFunc
def cc_binary_genrule(name, srcs=None, deps=None, cc_flags=None, cc_linker=None,
                      cc_linker_extra_lib_flags=None, objcopy=None, strip=None,
                      cc_compiler=None, cc_compiler_environment=None,
                      cflags=None, ccflags=None):
  if srcs is None:
    srcs = []
  if deps is None:
    deps = []
  if cc_flags is None:
    cc_flags = []
  rule = GenRule(name)
  binary = expand_out_path(name)
  binary_debug = expand_out_path(name + '.debug')

  cc_srcs = []
  h_srcs = []
  for src in srcs:
    if __is_c_or_cc(src):
      cc_srcs.append(src)
    elif src[-2:] == '.h':
      h_srcs.append(expand_path(src))
    else:
      raise RuleError('Invalid file extension for cc_library: ' + src)

  private_hdrs_fileset=None
  if h_srcs:
    hdr_rule = GenRule(name + '_hdrs')
    hdr_rule.inp.cc_private_hdrs.add_all(h_srcs)
    hdr_rule.out.cc_private_hdrs.depends_on(hdr_rule.inp.cc_private_hdrs)
    private_hdrs_fileset = hdr_rule.out.cc_private_hdrs

  for src in cc_srcs:
    o_file = expand_out_path(name + '--' + __to_o_file(src))
    rule_name = name + '--' + __to_o_file(src)
    src_rule = __cc_o_file_raw(name=rule_name, src_filenames=[expand_path(src)],
                               private_hdrs_fileset=private_hdrs_fileset,
                               o_file=o_file, deps=deps, cc_flags=cc_flags,
                               cc_compiler=cc_compiler,
                               cc_compiler_environment=cc_compiler_environment,
                               cflags=cflags, ccflags=ccflags)
    rule.inp.cc_o_lib.depends_on(src_rule.out.cc_o_lib)

  for dep in deps:
    dep_rule = getRule(dep)
    rule.inp.cc_a_lib.depends_on(dep_rule.out.cc_a_lib)
    rule.meta.cc_system_so_flags.depends_on(dep_rule.meta.cc_system_so_flags)
    rule.meta.deb_system_dep.depends_on(dep_rule.meta.deb_system_dep)

  cmd = cc_linker
  if cmd is None:
    raise RuleError('cc_linker must be specified in the config file.')

  cmd += [ '-o', binary]
  cmd += rule.inp.cc_o_lib.file_names()
  cmd += rule.inp.cc_a_lib.file_names()
  cmd += cc_linker_extra_lib_flags
  cmd += rule.meta.cc_system_so_flags.file_names()
  cmd += ['&&']
  cmd += objcopy
  cmd += ['--only-keep-debug', binary, binary_debug]
  cmd += ['&&']
  cmd += strip
  cmd += [binary]
  cmd += ['&&']
  cmd += objcopy
  cmd += ['--add-gnu-debuglink=' + binary_debug, binary]

  # TODO(austin): Pass the environment into the rule.

  rule.set_cmd(['/bin/bash', '-c', ' '.join(cmd)])
  rule.out.executable.add(binary)
  rule.out.debug.add(binary_debug)

  rule.out.data.depends_on(rule.out.executable)
  rule.out.data.depends_on(rule.out.debug)

  return rule

@GenRuleFunc
def bundle(name, srcs=None, deps=None, pattern=None, replace=None, strip=None,
           flatten=False, prefix='', match=None, reject=None, copy=False):
  """A bundle is a collection of files in a structure.

  Args:
    name: The rule name.
    srcs: A list of files.
    deps: A list of targets to collect the output data of.
    pattern: A regex used for modifying the names of files, or None.
    replace: The replacement to use with patten, or None.
    strip: int or None.  If provided, this many folders will be removed from
        the beginning of all paths.
    flatten: If True, strip all leading paths off the paths in srcs and deps.
        If False, preserve the path from the base of the repo.
    prefix: A prefix to prepend to the files.  This is applied after flattening.
    match: A regex that must match for the path to be included.
    reject: A regex that must match for the path to be excluded.
    copy: If true, copy the files instead of hard linking them.
  """
  if srcs is None:
    srcs = []
  if deps is None:
    deps = []

  # Array of lists of src, dst for cp/hard link
  dst_set = set()
  cp_list = []

  # If a dep is a fileset, strip the path leading up to the folder with the sources in it.
  # If it isn't, use the full path.
  # Should be able to do this by using a separate output.

  if '..' in prefix:
    raise RuleError('.. not allowed in a prefix')

  rule = GenRule(name)
  for dep in deps:
    dep_rule = getRule(dep)
    rule.meta.cc_system_so_flags.depends_on(dep_rule.meta.cc_system_so_flags)
    rule.meta.deb_system_dep.depends_on(dep_rule.meta.deb_system_dep)
    for data in dep_rule.out.data.file_names():
      out_path = output_basename() + '/'
      data_computed_path = data[len(out_path):]

      if pattern is not None:
        data_computed_path = re.sub(pattern, replace or '', data_computed_path)

      if flatten:
        dst = os.path.join(prefix, os.path.basename(data_computed_path))
      elif strip is not None:
        raise RuleError('strip not yet supported')
      else:
        dst = os.path.join(prefix, data_computed_path)

      if match is not None:
        if not re.search(match, dst):
          continue
      if reject is not None:
        if re.search(reject, dst):
          continue

      if dst in dst_set:
        raise RuleError('2 rules provide ' + dst)
      dst_set.add(dst)
      cp_list.append(
          (data, expand_out_path(os.path.join(name, dst)), dep_rule))

    for bundle_data in dep_rule.out.bundle.file_names():
      bundle_path = expand_out_path_for_rule(dep_rule, dep_rule.rulename() + '/')
      if not bundle_data.startswith(bundle_path):
        raise RuleError('Corrupted bundle \'%s\', should start with \'%s\'.' %
                        (bundle_data, bundle_path))
      bundle_computed_path = bundle_data[len(bundle_path):]

      if pattern is not None:
        bundle_computed_path = re.sub(pattern, replace or '',
                                      bundle_computed_path)

      if flatten:
        data = os.path.basename(bundle_computed_path)
        dst = os.path.join(prefix, data)
      elif strip is not None:
        raise RuleError('strip not yet supported')
      else:
        dst = os.path.join(prefix, bundle_computed_path)

      if match is not None:
        if not re.search(match, dst):
          continue
      if reject is not None:
        if re.search(reject, dst):
          continue

      if dst in dst_set:
        raise RuleError('2 rules provide ' + dst)
      dst_set.add(dst)
      cp_list.append(
          (bundle_data, expand_out_path(os.path.join(name, dst)), dep_rule))

  for src in srcs:
    dst = src
    if pattern is not None:
      dst = re.sub(pattern, replace or '', dst)

    if flatten:
      dst = os.path.join(prefix, os.path.basename(dst))
    elif strip is not None:
      raise RuleError('strip not yet supported')
    else:
      dst = os.path.join(prefix, dst)

    if match is not None:
      if not re.search(match, dst):
        continue
    if reject is not None:
      if re.search(reject, dst):
        continue

    if dst in dst_set:
      raise RuleError('2 rules provide ' + dst)
    dst_set.add(dst)
    cp_list.append(
        (expand_path(src), expand_out_path(os.path.join(name, dst)), None))

  for src, dst, src_rule in cp_list:
    single_cp_rule = GenRule(name + dst)
    if src_rule:
      single_cp_rule.inp.data.depends_on(src_rule.out.data)
      single_cp_rule.inp.bundle.depends_on(src_rule.out.bundle)
    else:
      single_cp_rule.inp.data.add(src)
    if copy:
      single_cp_rule.set_cmd(['cp', '-p', src, dst])
    else:
      single_cp_rule.set_cmd(['ln', src, dst])
    single_cp_rule.out.bundle.add(dst)
    rule.inp.bundle.depends_on(single_cp_rule.out.bundle)

  rule.out.bundle.depends_on(rule.inp.bundle)
  return rule

@GenRuleFunc
def zip_pkg(name, deps=None):
  my_bundle = bundle(name + "_bundle", deps=deps)
  return __compressed_pkg(name, my_bundle, '.zip', 'zip -q')

@GenRuleFunc
def targz_pkg(name, deps=None):
  my_bundle = bundle(name + "_bundle", deps=deps)
  return __compressed_pkg(name, my_bundle, '.tar.gz', 'tar czf')

@GenRuleFunc
def tarbz_pkg(name, deps=None):
  my_bundle = bundle(name + "_bundle", deps=deps)
  return __compressed_pkg(name, my_bundle, '.tar.bz2', 'tar cjf')

def __compressed_pkg(name, my_bundle, extension, cmd):
  if my_bundle is None:
    return GenRule(name)

  rule = GenRule(name)

  rule.inp.bundle.depends_on(my_bundle.out.bundle)
  zip_relative_file = name + extension
  zip_file = expand_out_path(zip_relative_file)
  bundle_path = expand_out_path_for_rule(my_bundle, my_bundle.rulename() + '/')
  zip_files = []
  for zipped_file in rule.inp.bundle.file_names():
    if not zipped_file.startswith(bundle_path):
      raise RuleError('Corrupted bundle.')
    zip_files.append(zipped_file[len(bundle_path):])

  rule.set_cmd(['bash', '-c',
                'cd %s && %s ../%s %s' % (
                    bundle_path, cmd, zip_relative_file, ' '.join(zip_files))])
  rule.out.data.add(zip_file)

  return rule

@GenRuleFunc
def deb_pkg(name, version=None, deps=None, maintainer=None, section='main',
            homepage=None, description=None, architecture=None,
            postinst=None):
  if homepage is None:
    homepage = option('deb_pkg_homepage', None)
    if homepage is None:
      raise RuleError('Must specify debian package homepage.')

  if architecture is None:
    architecture = option('deb_pkg_architecture', None)
    if architecture is None:
      raise RuleError('Must specify debian package architecture.')

  if version is None:
    raise RuleError('Must specify debian package version.')

  if description is None:
    raise RuleError('Must specify debian package description.')
  if maintainer is None:
    raise RuleError('Must specify debian package maintainer.')
  if '_' in name:
    raise RuleError('Packages names can not contain \'_\'')
  if '_' in version:
    raise RuleError('Packages versions can not contain \'_\'')

  # Make the control file.
  control_name = name + '-debian'
  control_folder = os.path.join(control_name, 'DEBIAN')
  control_output = expand_out_path(os.path.join(control_folder, 'control'))
  control_rule = GenRule(control_name)
  control_rule.out.bundle.add(control_output)

  # Generating the control file could be a parallel rule, but that seems
  # excessive.  When making the control file, link the postinst file over
  # if it has been provided.
  if not postinst:
    postinst_cmd = ''
  else:
    inp_postinst = expand_path(postinst)
    control_rule.inp.data.add(inp_postinst)
    postinst_file = expand_out_path(os.path.join(control_folder, 'postinst'))
    postinst_cmd = '&& cp {postinst} {control_postinst} && chmod 755 {control_postinst}'.format(
        postinst=inp_postinst, control_postinst=postinst_file)
    control_rule.out.bundle.add(postinst_file)

  # Create a folder with all the contents, including the DEBIAN folder
  # from above.
  bundle_folder = name + '_' + version
  deb_file = name + '_' + version + '.deb'
  my_bundle = bundle(bundle_folder, deps=deps + [':' + control_name], copy=True)

  control_rule.set_cmd(
    ['bash', '-c',
     "mkdir -p {debian_dir} && echo -e 'Package: {package}\n"
     "Version: {version}\n"
     "Maintainer: {maintainer}\n"
     "Section: {section}\n"
     "Priority: standard\n"
     "Architecture: {architecture}\n"
     "Homepage: {homepage}\n"
     "Depends: {depends}\n"
     "Description: {description}' > {output} && chmod 755 {output}".format(
         package=name,
         version=version,
         maintainer=maintainer,
         section=section,
         architecture=architecture,
         homepage=homepage,
         depends=', '.join(my_bundle.meta.deb_system_dep.file_names()),
         description=description,
         debian_dir=expand_out_path(control_folder),
         output=control_output) + postinst_cmd])

  rule = GenRule(name)
  rule.inp.data.depends_on(my_bundle.out.bundle)
  rule.out.data.add(expand_out_path(deb_file))
  cmd = ['fakeroot', 'bash', '-c',
         'find {bundle_folder} -perm /o+r -type f -execdir chmod a+r {{}} + && '
         'chown -R root.root {bundle_folder} &&'
         'dpkg-deb -b {bundle_folder}'.format(
             bundle_folder=expand_out_path(bundle_folder))]
  rule.set_cmd(cmd)

  return rule

@GenRuleFunc
def cc_test(name, srcs=[], deps=[], cc_flags=[], data=[], timeout=60):
  rule = cc_binary(name, srcs=srcs, deps=deps, cc_flags=cc_flags)
  rule.inp.data.add_all([expand_path(d) for d in data])
  rule.out.tmpdir.add(expand_out_path(name + '--tmpdir/'))

  data_path = None
  if data:
    bundle_name = name + '--data'
    data_path = expand_out_path(bundle_name)
    data_bundle = bundle(bundle_name, deps=data)
    rule.inp.bundle.depends_on(data_bundle.out.bundle)
    rule.out.bundle.depends_on(rule.inp.bundle)

  test_cmd = list(rule.out.executable.files)
  flag_cmd = option('cc_test_wrapper', None)
  if flag_cmd is not None:
    test_cmd = flag_cmd(test_cmd, rule, data_path, output_basename())
  test_cmd = ['timeout', str(timeout)]  + test_cmd
  rule.out.test.add_cmd(tuple(test_cmd))
  return rule

@GenRuleFunc
def cp(name, src, dst):
  rule = GenRule(name)
  rule.inp.data.add(expand_path(src))
  rule.out.data.add(expand_out_path(dst))
  rule.set_cmd(['ln', expand_path(src), expand_out_path(os.path.join(dst))])
  return rule

@GenRuleFunc
def git_revision(name, variable_name='git_rev_autogen'):
  out_cc_file = name + '.cc'
  out_o_file = name + '.o'
  cc_gen_rule = GenRule(out_cc_file)
  cc_gen_rule.wipe.head.add('.git/HEAD')

  with open('.git/HEAD') as f:
    revision = f.read()
  if revision.startswith('ref: '):
    # We now need to pull the revision from the rest of the contents of the
    # file.
    revision_file = os.path.join('.git', revision[5:-1])
  else:
    revision_file = '.git/HEAD'
  cc_gen_rule.inp.data.add(revision_file)
  expanded_out_cc_file = expand_out_path(out_cc_file)
  cc_gen_rule.out.cc_cc.add(expanded_out_cc_file)

  cmd = ['bash', '-c',
         'echo "char {variable_name}[] = \\"$(cat {revision_file})\\";" '
         '> {out_cc_file}'.format(
             variable_name=variable_name,
             revision_file=revision_file,
             out_cc_file=expanded_out_cc_file)]
  cc_gen_rule.set_cmd(cmd)

  o_rule = __cc_o_file(name=out_o_file, src_fileset=cc_gen_rule.out.cc_cc,
                       o_file=expand_out_path(out_o_file), deps=[])

  return __cc_library(name=name, extra_src_rules=[o_rule])

@GenRuleFunc
def symlink(name, target):
  rule = GenRule(name)
  rule.out.data.add(expand_out_path(name))
  cmd = ['ln', '-s', target, expand_out_path(name)]
  rule.set_cmd(cmd)

  return rule

@GenRuleFunc
def py_library(name, srcs=[], deps=[]):
  # Make a bunch of hardlinks, and a rule with python output.
  rule = GenRule(name)

  for src in srcs:
    single_cp_rule = GenRule(src)

    single_cp_rule.inp.py_library.add(expand_path(src))
    single_cp_rule.out.py_library.add(expand_out_path(src))

    single_cp_rule.set_cmd(['ln', expand_path(src), expand_out_path(src)])
    rule.inp.py_library.depends_on(single_cp_rule.out.py_library)

  for dep in deps:
    dep_rule = getRule(dep)
    rule.inp.py_library.depends_on(dep_rule.out.py_library)
    rule.meta.deb_system_dep.depends_on(dep_rule.meta.deb_system_dep)

  rule.out.py_library.depends_on(rule.inp.py_library)

  return rule

@GenRuleFunc
def py_binary(name, src, main='main', deps=[]):
  # Make a hard link and a trampoline which calls the provided main method in
  # the module.

  # Now, we need a bundle of hard links, placed in a folder called name_bundle

  # Then, generate a trampoline which imports src and calls main, and put that
  # in name_bundle/__main__.py

  # Then generate a symlink from name to name_bundle/__main__.py.

  # Then, generate a python zip file with #!/usr/bin/python3 at the front, and
  # the contents of name_bundle in it.
  if src[-3:] != '.py':
    raise RuleError('Python source file must end in .py')

  bundle_path = expand_out_path(name + '_bundle')

  main_file = os.path.join(bundle_path, '__main__.py')
  main_src = expand_path(src)[:-3].replace('/', '.')

  main_genrule = GenRule(name + '_bundle/__main__.py')

  if '(' not in main or ')' not in main:
    main += '()'

  cmd = ['bash', '-c',
         ('echo -e "#!/usr/bin/python3\n'
          '#main generated by ocean.\n'
          'import {module}\n'
          '{module}.' + main + '" > {main_file} && chmod a+x {main_file}').format(
              module=main_src,
              main=main,
              main_file=main_file)]

  main_genrule.set_cmd(cmd)
  main_genrule.out.bundle.add(main_file)

  # Link src into the bundle
  src_out_path = os.path.join(bundle_path, expand_path(src))

  src_cp_rule = GenRule(src)
  src_cp_rule.inp.py_library.add(expand_path(src))
  src_cp_rule.out.bundle.add(src_out_path)
  src_cp_rule.set_cmd(['ln', expand_path(src), src_out_path])

  # Main output rule.
  rule = GenRule(name)

  python_package = option('python_package', None)
  if python_package:
    dep_rule = getRule(python_package)
    rule.meta.deb_system_dep.depends_on(dep_rule.meta.deb_system_dep)

  for dep in deps:
    dep_rule = getRule(dep)
    rule.meta.deb_system_dep.depends_on(dep_rule.meta.deb_system_dep)
    rule.inp.py_library.depends_on(dep_rule.out.py_library)

  output_basedir = output_basename() + '/'
  for python_file in rule.inp.py_library.file_names():
    # Fun times.  The easy way to do this all is to then do copies from
    # rule.inp.py_library.  This has the problem that we will then require _all_
    # source coppies to finish before starting on building this package.  This
    # isn't the end of the world, but this isn't very good.

    if not python_file.startswith(output_basedir):
      raise RuleError(
          'Corrupted python lib file \'%s\', '
          'should start with \'%s\'' % (python_file, output_basedir))

    computed_python_file = python_file[len(output_basedir):]

    dst = os.path.join(bundle_path, computed_python_file)

    single_cp_rule = GenRule(name + computed_python_file)
    single_cp_rule.inp.data.depends_on(rule.inp.py_library)

    single_cp_rule.set_cmd(['ln', python_file, dst])
    single_cp_rule.out.bundle.add(dst)

    rule.inp.bundle.depends_on(single_cp_rule.out.bundle)

  rule.inp.bundle.depends_on(src_cp_rule.out.bundle)
  rule.inp.bundle.depends_on(main_genrule.out.bundle)

  pyz_path = expand_out_path(name + '.pyz')
  package_cmd = []
  package_cmd += ['cd', bundle_path, '&&']
  package_cmd += ['zip', '../' + name + '.pyz', '-r', '.', '&&']
  package_cmd += ['sed -i \'1s;^;#!/usr/bin/python3\\n;\' ../' +name + '.pyz', '&&']
  package_cmd += ['cd', '..', '&&']
  package_cmd += ['chmod', 'a+x', name + '.pyz', '&&']
  package_cmd += ['ln', '-s', name + '_bundle/__main__.py', name]
  rule.set_cmd(['/bin/bash', '-c', ' '.join(package_cmd)])

  rule.out.symlink.add(expand_out_path(name))
  rule.out.executable.add(expand_out_path(name + '.pyz'))
  rule.out.data.depends_on(rule.out.executable)

  return rule

@GenRuleFunc
def py_test(name, src, deps=[], main=None, data=[], timeout=60):
  # Make a py_binary, attach the data, and declare it a test.
  if main is None:
    main = option('py_test_main', 'unittest.main({module})')

  rule = py_binary(name=name, src=src, deps=deps, main=main)

  data_path = None
  if data:
    bundle_name = name + '--data'
    data_path = expand_out_path(bundle_name)
    data_bundle = bundle(bundle_name, deps=data)
    rule.inp.bundle.depends_on(data_bundle.out.bundle)
    rule.out.bundle.depends_on(rule.inp.bundle)

  rule.out.tmpdir.add(expand_out_path(name + '--tmpdir/'))

  test_cmd = list(rule.out.executable.files)
  flag_cmd = option('py_test_wrapper', None)
  if flag_cmd is not None:
    test_cmd = flag_cmd(test_cmd, rule, data_path, output_basename())
  test_cmd = ['timeout', str(timeout)]  + test_cmd
  rule.out.test.add_cmd(tuple(test_cmd))
  return rule
